Fix slash command autofill broken after containerless rewrite#332
Fix slash command autofill broken after containerless rewrite#332brendanlong merged 3 commits intomainfrom
Conversation
When claude-runner.ts was rewritten to use the Agent SDK directly in-process (removing the old agent-service container architecture), the slash command extraction and SSE emission logic was not migrated. The UI infrastructure (PromptInput autocomplete, SSE subscriptions) was intact but the server never populated commands. Fix by: 1. Calling q.supportedCommands() after starting a query to get rich command metadata (name, description, argumentHint) 2. Extracting slash_commands from the system init message and merging with the rich commands (init message has ALL commands, but as bare strings without descriptions) 3. Emitting merged commands via sseEvents.emitCommands() Also adds mergeSlashCommands() helper with tests. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Code Review
This pull request introduces a mergeSlashCommands utility to combine rich command metadata from the SDK with command names extracted from system initialization messages. The review identified a potential race condition where the asynchronous SDK response could overwrite commands discovered during the initialization loop, and suggested an improvement to ensure the mergeSlashCommands function correctly handles duplicate entries in the input array.
src/server/services/claude-runner.ts
Outdated
| .then((commands) => { | ||
| supportedCommands = commands; | ||
| sseEvents.emitCommands(sessionId, commands); | ||
| log.info('Emitted supported commands from SDK', { sessionId, count: commands.length }); |
There was a problem hiding this comment.
There is a race condition between the asynchronous supportedCommands() call and the processing of the system init message in the loop. If the init message arrives first, supportedCommands is populated with the full list of names. When the SDK promise later resolves, it currently overwrites the variable with only the rich metadata subset, potentially losing commands that were only present in the init message (e.g., /compact, /cost). Merging the results in the .then block prevents this data loss.
.then((commands) => {
// Merge with any names already discovered from system init messages
const currentNames = supportedCommands.map((c) => c.name);
supportedCommands = mergeSlashCommands(commands, currentNames);
sseEvents.emitCommands(sessionId, supportedCommands);
log.info('Emitted supported commands from SDK', { sessionId, count: commands.length });
})References
- To prevent race conditions in methods with asynchronous initialization, set a processing/locked flag synchronously at the start of the method, before any await calls, to ensure concurrent invocations are rejected immediately.
| for (const name of slashCommandNames) { | ||
| if (!existingNames.has(name)) { | ||
| merged.push({ name, description: '', argumentHint: '' }); | ||
| } | ||
| } |
There was a problem hiding this comment.
The current implementation of mergeSlashCommands does not handle potential duplicates in the slashCommandNames array. If a name appears multiple times in the input array and is not already present in existingCommands, it will be added multiple times to the result. Updating the existingNames set within the loop ensures that each new command is only added once.
| for (const name of slashCommandNames) { | |
| if (!existingNames.has(name)) { | |
| merged.push({ name, description: '', argumentHint: '' }); | |
| } | |
| } | |
| for (const name of slashCommandNames) { | |
| if (!existingNames.has(name)) { | |
| merged.push({ name, description: '', argumentHint: '' }); | |
| existingNames.add(name); | |
| } | |
| } |
Address PR review feedback: - Fix race condition: when supportedCommands() resolves after the system init message, merge with already-discovered names instead of overwriting them - Fix duplicate handling: add names to the existingNames set during iteration to prevent duplicates from slashCommandNames array Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Cache discovered commands on SessionState so the getCommands endpoint returns them on page reload (not just via SSE) - Use SystemInitContentSchema.safeParse() instead of manual property checks for type-safe init message parsing - Replace length-based heuristic with actual set comparison to detect new commands accurately Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Summary
claude-runner.tswas rewritten for the containerless architecture, the slash command extraction and SSE emission logic was not migrated from the oldagent-service/did nothingq.supportedCommands()for rich metadata, extractslash_commandsfrom system init message, merge both sources, and emit viasseEvents.emitCommands()mergeSlashCommands()helper with 5 unit testsTest plan
pnpm test:run)/in the prompt input — autocomplete dropdown should appear with commands like/compact,/commit,/review, etc./commit) show descriptions in the dropdown/compact,/cost) still appear in the dropdown🤖 Generated with Claude Code